package com.octeva.core.orm.hibernate3.search;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.DateTools.Resolution;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;

import com.octeva.core.orm.hibernate3.Entity;

public class SearchUtils {
	
	private SearchUtils() {}
	
	/**
	 * create a always true query.
	 * 
	 * It is useful when user type nothing in search inputs 
	 * and we want to return all entities.
	 * 
	 * @param entityClass
	 * @return
	 */
	public static Query createAlwaysTrueQuery(Class<? extends Entity> entityClass) {
		return new TermQuery(new Term("_hibernate_class", entityClass.getCanonicalName()));
	}
	
	/**
	 * 123.1 => 000012310
	 * @param number
	 * @param scale
	 * @param precision
	 * @return
	 */
	public static String numberToString(Float number, int scale, int precision) {

		if (number != null) {
			String str = number.toString();
			int strlen = str.length();
			int index = str.indexOf(".");
			int p1 = -1, p2 = -1;
			if (index == -1) {
				p1 = scale-strlen;
				p2 = precision;
//				str += ".";
			} else {
				p1 = scale-index;
				p2 = precision-(strlen-index-1);
			}
			for (int i = 0; i < p1; i ++) {
				str = "0" + str;
			}
			for (int i = 0; i < p2; i ++) {
				str = str + "0";
			}
			return str;
		}
		return null;
	}
	
	private static Map<String, String> splitDateString(Date value, Resolution resolution) {
		
		String date = DateTools.dateToString(value, resolution);
		return splitDateString(date);
	}
		
	private static Map<String, String> splitDateString(String date) {
		
		Map<String, String> map = new HashMap<String, String>();
		if (date.length() > 3) {
			map.put("Year", date.substring(0, 4));
		}
		if (date.length() > 5) {
			map.put("Month", date.substring(4, 6));
		}
		if (date.length() > 7) {
			map.put("Day", date.substring(6, 8));
		}
		if (date.length() > 9) {
			map.put("Hour", date.substring(8, 10));
		}
		if (date.length() > 11) {
			map.put("Minute", date.substring(10, 12));
		}
		if (date.length() > 13) {
			map.put("Second", date.substring(12, 14));
		}
		if (date.length() > 16) {
			map.put("MilliSecond", date.substring(14, 17));
		}
		return map;
	}
	
	public static List<Field> createDateFields(String name, Date value, Resolution resolution,
			Store store, Index index, Float boost) {
		
		List<Field> fields = new ArrayList<Field>();
		if (value != null) {
			Map<String, String> map = splitDateString(value, resolution);
			//append to document
			for (String key: map.keySet()) {
				Field field = new Field(name + key, map.get(key), 
									store, index, Field.TermVector.YES);
				if (boost != null) field.setBoost(boost);
				fields.add(field);
			}
		}
		return fields;
	}
	
	public static List<Field> createDateFields(String name, 
			Date value, Resolution resolution) {
		
		return createDateFields(name, value, resolution, Store.NO, Index.NOT_ANALYZED, null);
	}
	
	private static String[][] getDateIntervals() {

		return new String[][] {
			new String[] {"Year", 			"1970", "9999"}, //year			: (1900, 9999)
			new String[] {"Month",			  "00",   "13"}, //month 		: (  00,   13)
			new String[] {"Day", 			  "00",   "32"}, //day			: (  00,   32) 
			new String[] {"Hour", 			  "##",	  "24"}, //hour			: (  ##,   24)
			new String[] {"Minute", 		  "##",	  "60"}, //minute		: (  ##,   60) 
			new String[] {"Second", 		  "##",	  "60"}, //second		: (  ##,   60)
			new String[] {"MilliSecond", 	 "###",	 "AAA"}, //milliSecond	: ( ###,  AAA)
		};
	}
	
	private static int getResolutionIndex(Resolution resolution) {

		String last = null;
		if (resolution == Resolution.YEAR) last = "Year";
		else if (resolution == Resolution.MONTH) last = "Month";
		else if (resolution == Resolution.DAY) last = "Day";
		else if (resolution == Resolution.HOUR) last = "Hour";
		else if (resolution == Resolution.MINUTE) last = "Minute";
		else if (resolution == Resolution.SECOND) last = "Second";
		else last = "MilliSecond";	//resolution == Resolution.MILLISECOND
		
		String[][] intervals = getDateIntervals();
		for (int i = 0; i < intervals.length; i ++) {
			if (last.equalsIgnoreCase(intervals[i][0])) {
				return i+1;
			}
		}
		throw new RuntimeException("Exception never be thrown!");
	}
	
	public static Query createLowerBoundQuery(String dateField, String startDate, 
			Resolution resolution, boolean include) {
		
		//start date
		if (startDate == null) {
			Calendar calendar = Calendar.getInstance();
			calendar.setTimeInMillis(0);
			startDate = DateTools.dateToString(calendar.getTime(), resolution);	//1970/01/01 00:00:00.000
		}

		Map<String, String> map = splitDateString(startDate);
		return createDateLowerBoundQuery(dateField, resolution, include, map);
	}
	
	/**
	 * here, dateField are stored in lucene index in three fields: "Day", "Month", "Year".
	 * 
	 * <br/>please note this: 
	 * <br/><code>date1 >= date2</code>
	 * <br/>only and if only
	 * <br/><code>year1>year2 
	 * 	|| (year1 == year2 && month1 > month2) 
	 * 	|| (year1 == year2 && month1 == month2 && day1 >= day2)</code>
	 *
	 * this approach is used to avoid the code throw exception below:
	 * <br/>"org.apache.lucene.search.BooleanQuery$TooManyClauses: maxClauseCount is set to 1024" 
	 *  
	 * @param dateField
	 * @param startDate
	 * @param resolution
	 * @param include		include the startDate or not
	 * @return
	 */
	public static Query createLowerBoundQuery(String dateField, Date startDate, 
			Resolution resolution, boolean include) {

		//start date
		if (startDate == null) {
			Calendar calendar = Calendar.getInstance();
			calendar.setTimeInMillis(0);
			startDate = calendar.getTime();	//1970/01/01 00:00:00.000
		}

		Map<String, String> map = splitDateString(startDate, Resolution.MILLISECOND);
		return createDateLowerBoundQuery(dateField, resolution, include, map);
	}

	private static Query createDateLowerBoundQuery(String dateField,
			Resolution resolution, boolean include, Map<String, String> mapDate) {
		
		String[][] dateIntervals = getDateIntervals();
		int index = getResolutionIndex(resolution);
		
		//create query string
		String query = "";
		for (int i = 0; i < index; i ++) {
			query += "(";
			for (int j = 0; j < i; j ++) {
				query += "+(#fieldName:#fieldValue) "	//do not remove the last blank
					.replaceAll("\\#fieldName", dateField + dateIntervals[j][0])
					.replaceAll("\\#fieldValue", mapDate.get(dateIntervals[j][0]));
			}
			//+(#dateFieldMonth: {#startMonth TO 13})
			if (i == index-1 && include) {
				query += "+(#fieldName:[#fieldValue1 TO #fieldValue2]) "	//do not remove the last blank
					.replaceAll("\\#fieldName", dateField + dateIntervals[i][0])
					.replaceAll("\\#fieldValue1", mapDate.get(dateIntervals[i][0]))
					.replaceAll("\\#fieldValue2", dateIntervals[i][2]);
			} else {
				query += "+(#fieldName:{#fieldValue1 TO #fieldValue2}) "	//do not remove the last blank
					.replaceAll("\\#fieldName", dateField + dateIntervals[i][0])
					.replaceAll("\\#fieldValue1", mapDate.get(dateIntervals[i][0]))
					.replaceAll("\\#fieldValue2", dateIntervals[i][2]);
			}
			query += ") ";	//do not remove the last blank
		}

		//return
		try {
			Query q = new QueryParser("", new StandardAnalyzer()).parse(query);
			return q;
		} catch (org.apache.lucene.queryParser.ParseException e) {
			throw new RuntimeException(e);
		}
	}
	
	public static Query createUpperBoundQuery(String dateField, String endDate, 
			Resolution resolution, boolean include) {

		//start date
		if (endDate == null) {
			Calendar calendar = Calendar.getInstance();
			calendar.set(9999, 11, 31, 0, 0, 0);
			calendar.set(Calendar.MILLISECOND, 0);
			endDate = DateTools.dateToString(calendar.getTime(), resolution);	//9999/12/30 16:00:00 GMT+0800
		}

		Map<String, String> map = splitDateString(endDate);
		return createDateUpperBoundQuery(dateField, resolution, include, map);
	}

	/**
	 * here, dateField are stored in lucene index in three fields: "Day", "Month", "Year".
	 * 
	 * <br/>please note this: 
	 * <br/><code>date1 >= date2</code>
	 * <br/>only and if only
	 * <br/><code>year1>year2 
	 * 	|| (year1 == year2 && month1 > month2) 
	 * 	|| (year1 == year2 && month1 == month2 && day1 >= day2)</code>
	 *
	 * this approach is used to avoid the code throw exception below:
	 * <br/>"org.apache.lucene.search.BooleanQuery$TooManyClauses: maxClauseCount is set to 1024" 
	 *  
	 * @param dateField
	 * @param endDate
	 * @param resolution
	 * @param include		include the startDate or not
	 * @return
	 */
	public static Query createUpperBoundQuery(String dateField, Date endDate, 
			Resolution resolution, boolean include) {

		//start date
		if (endDate == null) {
			Calendar calendar = Calendar.getInstance();
			calendar.set(9999, 11, 31, 0, 0, 0);
			calendar.set(Calendar.MILLISECOND, 0);
			endDate = calendar.getTime();	//9999/12/30 16:00:00 GMT+0800
		}

		Map<String, String> map = splitDateString(endDate, Resolution.MILLISECOND);
		return createDateUpperBoundQuery(dateField, resolution, include, map);
	}

	private static Query createDateUpperBoundQuery(String dateField,
			Resolution resolution, boolean include, Map<String, String> mapDate) {
		String[][] dateIntervals = getDateIntervals();
		int index = getResolutionIndex(resolution);
		
		//create query string
		String query = "";
		for (int i = 0; i < index; i ++) {
			query += "(";
			for (int j = 0; j < i; j ++) {
				query += "+(#fieldName:#fieldValue) "	//do not remove the last blank
					.replaceAll("\\#fieldName", dateField + dateIntervals[j][0])
					.replaceAll("\\#fieldValue", mapDate.get(dateIntervals[j][0]));
			}
			//+(#dateFieldMonth: {#startMonth TO 13})
			if (i == index-1 && include) {
				query += "+(#fieldName:[#fieldValue1 TO #fieldValue2]) "	//do not remove the last blank
					.replaceAll("\\#fieldName", dateField + dateIntervals[i][0])
					.replaceAll("\\#fieldValue1", dateIntervals[i][1])
					.replaceAll("\\#fieldValue2", mapDate.get(dateIntervals[i][0]));
			} else {
				query += "+(#fieldName:{#fieldValue1 TO #fieldValue2}) "	//do not remove the last blank
					.replaceAll("\\#fieldName", dateField + dateIntervals[i][0])
					.replaceAll("\\#fieldValue1", dateIntervals[i][1])
					.replaceAll("\\#fieldValue2", mapDate.get(dateIntervals[i][0]));
			}
			query += ") ";	//do not remove the last blank
		}

		//return
		try {
			Query q = new QueryParser("", new StandardAnalyzer()).parse(query);
			return q;
		} catch (org.apache.lucene.queryParser.ParseException e) {
			throw new RuntimeException(e);
		}
	}
	
	public static List<Field> createNumberFields(String name, String value, Store store,
			Index index, Float boost) {
		
		List<Field> fields = new ArrayList<Field>();
		if (value != null) {
			//append to document
			char[] vs = value.toCharArray();
			for (int i = 0; i < vs.length; i ++) {
				Field field = new Field(name + i, String.valueOf(vs[i]),
											store, index, Field.TermVector.YES);
				if (boost != null) field.setBoost(boost);
				fields.add(field);
			}
		}
		return fields;
	}

	public static List<Field> createNumberFields(String name, String value) {
		
		return createNumberFields(name, value, Store.NO, Index.NOT_ANALYZED, null);
	}
	
	public static Query createMatchQuery(String numberField, 
			int length, String value) {

		if (value.length() != length) {
			throw new RuntimeException("the length of number 'lower' is not " + length);
		}

		//create query string
		/*
		 * query = 
		 *		(+(#field[0]:#l[0]) +(#field[1]:#l[1]) ... +(#field[n]:#l[n]))
		 *
		 */
		String query = "";
		char[] l = value.toCharArray();
		query += "(";
		for (int i = 0; i < length; i ++) {
			query += "+(" + numberField + i + ":" + l[i] + ") ";
		}
		query += ") ";
		
		//return
		try {
			Query q = new QueryParser("", new StandardAnalyzer()).parse(query);
			return q;
		} catch (org.apache.lucene.queryParser.ParseException e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * here, numberField are stored in lucene index in lots of fields: "numberField0" ~ "numberFieldn".
	 * 
	 * <br/>please note this: 
	 * <br/><code>num1 >= num2</code>
	 * <br/>only and if only
	 * <br/><code>num1_0 > num2_0 
	 * 	|| (num1_0 == num2_0 && num1_1 > num2_1)
	 *  ...... 
	 * 	|| (num1_0 == num2_0 && num1_1 > num2_1 && ... && num1_n > num2_n)</code>
	 *
	 * this approach is used to avoid the code throw exception below:
	 * <br/>"org.apache.lucene.search.BooleanQuery$TooManyClauses: maxClauseCount is set to 1024" 
	 *  
	 * @param numberField
	 * @param length		the length of the number field
	 * @param lower
	 * @param include		include the lower or not
	 * @return
	 */
	public static Query createLowerBoundQuery(String numberField, int length, 
		String lower, boolean include) {

		if (lower.length() != length) {
			throw new RuntimeException("the length of number 'lower' is not " + length);
		}

		//create query string
		/*
		 * query = 
		 *		(+(#field[0]:{#l[0] TO a}))
		 *		(+(#field[0]:#l[0]) +(#field[1]:{#l[1] TO a}))
		 *		(+(#field[0]:#l[0]) +(#field[1]:#l[1]) +(#field[2]:{#l[2] TO a}))
		 *		...
		 *		(+(#field[0]:#l[0]) +(#field[1]:#l[1]) ... +(#field[n]:[#l[n] TO a]))
		 *
		 */
		String query = "";
		char[] l = lower.toCharArray();
		for (int i = 0; i < length; i ++) {
			query += "(";
			for (int j = 0; j < i; j ++) {
				query += "+(" + numberField + j + ":" + l[j] + ") ";
			}
			if (i == length - 1) {
				if (include) {
					query += "+(" + numberField + i + ":[" + l[i] + " TO a]) ";
				} else {
					query += "+(" + numberField + i + ":{" + l[i] + " TO a}) ";
				}
			} else {
				query += "+(" + numberField + i + ":{" + l[i] + " TO a}) ";
			}
			query += ") ";
		}
		
		//return
		try {
			Query q = new QueryParser("", new StandardAnalyzer()).parse(query);
			return q;
		} catch (org.apache.lucene.queryParser.ParseException e) {
			throw new RuntimeException(e);
		}
	}

	public static Query createLowerBoundQuery(String name, 
			Float number, int scale, int precision, boolean include) {
		
		String value = numberToString(number, scale, precision);
		return createLowerBoundQuery(name, value.length(), value, include);
	}
	
	/**
	 * here, numberField are stored in lucene index in lots of fields: "numberField0" ~ "numberFieldn".
	 * 
	 * <br/>please note this: 
	 * <br/><code>num1 >= num2</code>
	 * <br/>only and if only
	 * <br/><code>num1_0 > num2_0 
	 * 	|| (num1_0 == num2_0 && num1_1 > num2_1)
	 *  ...... 
	 * 	|| (num1_0 == num2_0 && num1_1 > num2_1 && ... && num1_n > num2_n)</code>
	 *
	 * this approach is used to avoid the code throw exception below:
	 * <br/>"org.apache.lucene.search.BooleanQuery$TooManyClauses: maxClauseCount is set to 1024" 
	 *  
	 * @param numberField
	 * @param length		the length of the number field
	 * @param upper
	 * @param include		include the upper or not
	 * @return
	 */
	public static Query createUpperBoundQuery(String numberField, int length, 
		String upper, boolean include) {

		if (upper.length() != length) {
			throw new RuntimeException("the length of number 'lower' is not " + length);
		}

		//create query string
		/*
		 * query = 
		 *		(+(#field[0]:{_ TO u[0]}))
		 *		(+(#field[0]:#u[0]) +(#field[1]:{_ TO #u[1]}))
		 *		(+(#field[0]:#u[0]) +(#field[1]:#u[1]) +(#field[2]:{_ TO #u[2]}))
		 *		...
		 *		(+(#field[0]:#u[0]) +(#field[1]:#u[1]) ... +(#field[n]:[0 TO #u[n]]))
		 *
		 */
		String query = "";
		char[] u = upper.toCharArray();
		for (int i = 0; i < length; i ++) {
			query += "(";
			for (int j = 0; j < i; j ++) {
				query += "+(" + numberField + j + ":" + u[j] + ") ";
			}
			if (i == length - 1) {
				if (include) {
					query += "+(" + numberField + i + ":[# TO " + u[i] + "]) ";
				} else {
					query += "+(" + numberField + i + ":{# TO " + u[i] + "}) ";
				}
			} else {
				query += "+(" + numberField + i + ":{# TO " + u[i] + "}) ";
			}
			query += ") ";
		}

		//return
		try {
			Query q = new QueryParser("", new StandardAnalyzer()).parse(query);
			return q;
		} catch (org.apache.lucene.queryParser.ParseException e) {
			throw new RuntimeException(e);
		}
	}
	
	public static Query createUpperBoundQuery(String name, 
			Float number, int scale, int precision, boolean include) {
		
		String value = numberToString(number, scale, precision);
		return createUpperBoundQuery(name, value.length(), value, include);
	}
}
